---
id: 事件循环
title: 事件循环
---

事件循环(event loop)是 JavaScript 运行时的一种机制，用于处理异步任务和回调函数。它负责管理 JavaScript 运行时中的所有事件，包括用户输入、网络请求、计时器等等，并根据事件的类型和优先级，按照一定的顺序进行处理。

- JavaScript 运行时的事件循环包括以下几个主要的组成部分：
  - Call Stack（调用栈）：用于存储 JavaScript 代码的执行上下文（执行上下文包括函数、变量等信息），在执行 JavaScript 代码时，会从 Call Stack 中取出一个执行上下文，将其压入 Call Stack 中，并执行其中的代码。
  - Web APIs（Web API 模块）：Web APIs 模块是浏览器提供的一组 API，包括 setTimeout、setInterval、XMLHttpRequest 等，它们提供了一种异步执行代码的方式。当调用 Web API 时，会将其对应的任务加入到任务队列中，等待 Event Loop 机制去处理。
  - Task Queue（任务队列）：用于存储异步任务，当 Web API 模块完成异步任务后，将其加入到任务队列中。
  - Microtask Queue（微任务队列）：用于存储微任务，包括 Promise、MutationObserver 等，微任务通常需要在当前宏任务执行完毕后立即执行。

- 事件循环的基本流程如下：
  - JavaScript 引擎在执行主线程代码时，将代码依次压入 Call Stack 中执行。
  - 当遇到 Web API（如 setTimeout）时，将其对应的任务加入到任务队列中，然后继续执行主线程中的代码。
  - 当主线程的代码执行完毕后，会检查 Microtask Queue 中是否有微任务，如果有，则依次执行其中的微任务，直到 Microtask Queue 为空。
  - 接着，主线程会从任务队列中取出一个任务（通常是先进先出的方式），执行其中的代码。在执行过程中，如果遇到 Web API，则将其对应的任务加入到任务队列中。
  - 重复上述步骤，直到任务队列和 Microtask Queue 都为空。
  - 需要注意的是，事件循环是单线程执行的，即同一时间只能执行一个任务，而且所有的事件都是按照队列的顺序进行处理的，也就是说，如果一个事件正在执行，其他事件必须等待它完成才能继续执行。这种机制保证了 JavaScript 的可预测性和安全性。

必看[演示网站 ](https://www.jsv9000.app/)。

思考一下，下方两段代码的执行顺序。

```html
<!-- 手动点击提交  -->
<button id="btn1">提交</button>
<script>
  console.log(0);
  $("#btn").click(function (e) {
    console.log(1);
  });
  console.log(2);
</script>

<!-- 结果如下  -->
<!-- 0 -->
<!-- 2 -->
<!-- 1 -->
```

```js
console.log(0);

setTimeout(() => {
  console.log(1);
}, 2000);

console.log(2);

// 结果如下
// 0
// 2
// 1
```

JS 是单线程运行的, 在遇到事件和定时器这些异步会跳过去，那么当需要执行这些异步时，要怎么定位到需要执行的异步代码呢?  
又不是多线程。那么是从头来一遍，还是倒着来一遍，都不太行。  
解决办法就找个地方把异步代码存储下来，这个地方叫做队列(queue)，需要执行异步事件时，就去 queue 里面找。

## 事件循环任务的分类

### 微任务 （micro-task）

微任务是 JavaScript 引擎提供的任务，它们是在当前任务执行完毕后立即执行的任务，可以看做是一个在宏任务执行完成后的回调。它们的执行顺序优先于下一个宏任务，也就是说，当宏任务执行完成后，会先处理所有的微任务，然后再执行下一个宏任务。常见的微任务有：

- Promise.then 和 Promise.catch
- Object.observe
- MutationObserver

当一个宏任务执行过程中产生了多个微任务，这些微任务会被依次加入到微任务队列中，直到宏任务执行完毕。当调用栈为空时，事件循环会从微任务队列中依次取出微任务，并将其添加到调用栈中执行。

### 宏任务 （macro-task）

- 宏任务是由宿主环境（如浏览器）提供的任务，包括事件回调、定时器等。每个宏任务都有自己的执行上下文和执行顺序。当一个宏任务被执行时，会一直执行到完成，直到调用栈为空。常见的宏任务有：
- setTimeout 和 setInterval
- UI 渲染
- XMLHttpRequest 和 Fetch 请求
- I/O 操作

### 微任务 vs 宏任务

```js
// 先执行微任务再执行宏任务。
setTimeout(() => {
  alert("setTimeout 执行完毕");
}, 0);

let p = Promise.resolve().then(() => {
  alert("promise执行完毕");
});
```

### 微任务 vs 宏任务 vs DOM

```html
<script>
  // 关于微任务和宏任务发生在DOM渲染前或后，因浏览器而异。
  let dom = document.createElement("div");
  dom.innerText = "xxx";
  document.body.appendChild(dom);

  setTimeout(() => {
    alert("setTimeout 执行完毕");
  }, 0);

  let p = Promise.resolve().then(() => {
    alert("promise执行完毕");
  });
  alert("同步代码结束");
</script>
```

## Event Loop 的循环算法

**事件循环算法包括四个关键步骤(仅供参考)：**

- 评估脚本：同步执行脚本，就好像它是一个函数体一样。运行直到调用堆栈为空。
- 运行任务：从任务队列中选择最旧的任务。运行它直到调用堆栈为空。
- 运行所有微任务：从微任务队列中选择最旧的微任务。运行它直到调用堆栈为空。重复直到微任务队列为空。
- 重新渲染 UI：重新渲染 UI。然后，返回第 2 步。（此步骤仅适用于浏览器，不适用于 NodeJS)。

## Node 事件循环

Node.js 的事件循环（Event Loop）机制与浏览器中的事件循环机制类似，但在细节实现上略有不同。

在 Node.js 中，事件循环主要由以下四个阶段组成：

- Timer 阶段：检测 setTimeout 和 setInterval 是否到达预定时间，如果到达则将回调函数加入到下一个阶段的队列中。
- I/O 阶段：执行一些异步 I/O 相关的回调函数，如网络请求的回调函数、文件 I/O 的回调函数等。
- Idle, Prepare 阶段：这两个阶段一般不会被用到，可以忽略。
- Poll 阶段：等待新的 I/O 事件到达，执行与 I/O 相关的回调函数，如果没有任何 I/O 事件需要处理，将等待新的事件到达或者到达定时器设定的时间。
- Check 阶段：执行 <a href="#setImmediate">setImmediate</a> 函数的回调函数。
- Close Callbacks 阶段：执行一些关闭的回调函数，例如 socket.on('close', ...)。

### node事件循环处理网络请求案例
```js
const http = require('http');

http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.write('Hello World!');
  res.end();
}).listen(3000);

//1.我们使用 http 模块创建了一个 HTTP 服务器，并监听端口 3000。
//2.当有 HTTP 请求进来时，事件循环会将该事件加入到事件队列中，事件循环空闲时，会从事件队列中取出该事件并执行相应的回调函数。即处理该请求。
//3.当处理完请求并发送响应后，回调函数执行完毕，事件从队列中移除，事件循环继续等待下一个事件的到来。
//4.如果没有新的事件需要处理，事件循环会一直保持空闲状态，直到服务器关闭或者出现错误。
```

事件循环中的每个阶段都有一个对应的回调队列，当一个阶段的所有任务都执行完毕后，事件循环会进入下一个阶段并处理该阶段的回调队列。如果在某个阶段执行了 <a href="#setImmediate">setImmediate</a> 函数，那么事件循环会立即进入 Check 阶段，执行 <a href="#setImmediate">setImmediate</a> 函数的回调函数。而如果在某个阶段执行了 <a href="#process">process.nextTick</a> 函数，那么事件循环会在该阶段结束后立即进入下一轮循环，处理 <a href="#process">process.nextTick</a> 函数的回调函数。  
需要注意的是，每个阶段的执行时间是有上限的，如果某个回调函数的执行时间超过了阶段的最大执行时间，那么该阶段的后续回调函数会被阻塞，直到该回调函数执行完毕。因此，在编写具有大量计算密集型任务的应用程序时，需要特别注意控制每个回调函数的执行时间，避免阻塞事件循环的正常运行。


## 测试
### 为什么要区分宏任务和微任务?
  - js异步操作类型的执行时间是不确定的，因此我们需要一种机制来控制它们的执行顺序，从而保证代码的正确性和可维护性。

### Node.js 事件循环和 JavaScript 事件循环区别？
  - 实现方式不同：Node.js 事件循环和 JavaScript 事件循环的底层实现不同。Node.js 事件循环使用的是基于 <a href="#libuv"> libuv</a> 库的实现，而 JavaScript 事件循环使用的是浏览器环境下的实现。
  - API 不同：Node.js 和 JavaScript 事件循环的 API 不完全相同，Node.js 的事件循环 API 主要集中在 <a href="#process">process.nextTick</a> 和  <a href="#setImmediate">setImmediate</a> 上，而 JavaScript 事件循环的 API 主要集中在 setTimeout、setInterval、requestAnimationFrame 上。
  - 目的不同：Node.js 事件循环的主要目的是提供异步 IO 操作的能力，以充分利用计算机的并发能力；而 JavaScript 事件循环的主要目的是实现用户交互和动画效果等前端功能。

## 解释

### libuv {#libuv}
libuv是一个跨平台的C语言库，用于支持异步IO编程模型，是Node.js实现异步IO的核心库。libuv提供了事件循环、文件IO、网络IO、定时器等基本功能，通过其统一的接口，可以屏蔽底层操作系统差异，方便程序员进行跨平台开发。

在Node.js中，libuv扮演了非常重要的角色，实现了Node.js的事件循环机制，它为Node.js提供了非常高效的事件循环和异步IO支持，保证了Node.js的高并发、高性能等特性。

除了Node.js之外，libuv还被其他的一些软件所使用，例如，通过使用libuv，Microsoft Visual Studio Code等应用程序实现了在Windows、Linux和macOS等操作系统中的高效事件循环和异步IO。

### process {#process}

是Node.js的一个全局对象，它提供了有关当前Node.js进程的信息和控制，可以让我们通过代码对进程进行管理和监控。

process.nextTick函数会在当前操作结束后，在下一个事件循环的开始处立即执行回调函数。它可以用于在当前操作完成后，立即执行一些回调函数，例如在异步函数执行结束后立即执行回调函数，或者在处理完一个事件后立即执行回调函数等。需要注意的是，由于process.nextTick的回调函数执行优先级非常高，因此如果回调函数执行时间过长，可能会导致事件循环阻塞，影响应用程序的性能。


### setImmediate {#setImmediate}
 setImmediate函数会在事件循环的下一个迭代中执行回调函数。它可以用于在当前操作完成后，稍后执行一些回调函数，例如在下一个事件循环中执行回调函数，或者在事件循环的不同阶段执行回调函数等。需要注意的是，由于setImmediate的回调函数执行优先级比较低，因此它的回调函数可能会被延迟执行，直到其他任务完成。

###  <a href="#process">process.nextTick</a>和 <a href="#setImmediate">setImmediate</a> 案例
```js 
function doSomeAsyncOperation() {
    console.log('1');
}

setImmediate(() => {
    console.log('2');
});

setTimeout(() => {
    doSomeAsyncOperation();
}, 0);

process.nextTick(() => {
    console.log('3');
});

console.log('end of main thread');

// 输出
// end of main thread
// 3
// 1
// 2

```
## 参考

[事件循环：微任务和宏任务](https://zh.javascript.info/event-loop)  
[Node 事件循环](https://nodejs.org/zh-cn/docs/guides/event-loop-timers-and-nexttick/)